package cn.demoncat.util.pay.alipay;

import cn.demoncat.util.exception.BizErrorRuntimeException;
import cn.demoncat.util.exception.BizRuntimeException;
import cn.demoncat.util.exception.ParamRuntimeException;
import cn.demoncat.util.lang.*;
import cn.demoncat.util.lang.constant.CharsetConstant;
import cn.demoncat.util.lang.enums.DateFormatEnum;
import cn.demoncat.util.pay.alipay.config.AlipayProperties;
import cn.demoncat.util.pay.alipay.constant.AlipayConstant.*;
import cn.demoncat.util.pay.alipay.constant.AlipayLogMsg;
import cn.demoncat.util.pay.alipay.constant.AlipayMsg;
import cn.demoncat.util.pay.alipay.entity.*;
import cn.demoncat.util.web.HttpClientUtil;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.AlipayRequest;
import com.alipay.api.AlipayResponse;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.*;
import com.alipay.api.response.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * 支付宝工具
 * 
 * 文档：https://docs.open.alipay.com
 * 费率：按单笔计算；普通行业费率 0.6%，特殊行业费率 1%（游戏、交易平台）
 * 
 * @author 延晓磊
 *
 * @since 2019年9月24日
 */
@Component
public class AlipayUtil {

	@Resource
	private AlipayClient alipayClient;
	@Resource
	private AlipayProperties alipayProperties;
	
	private static final Logger LOG = LoggerFactory.getLogger(AlipayUtil.class);
 
	
	/**
	 * 申请支付
	 * <pre>
	 * 扫码支付流程：申请支付(S) > 展示支付页面(C) > 扫码支付(U) > 间隔查询服务端支付状态来刷新界面(C)/取消支付时返回到request_from_url(W)
	 * APP支付流程：申请支付(S) > 调起微信(C) > 扫码支付(U) > 间隔查询服务端支付状态来刷新界面(C)/取消支付时返回(C)
	 * H5支付流程：申请支付(S) > 展示支付页面(C) > 调起支付(U) > 间隔查询服务端支付状态来刷新界面(C)/取消支付时返回到quit_url(W)
	 * </pre>
	 * @param param
	 * @param type		支付类型
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月17日
	 */
	public AlipayResultPay pay(AlipayParamPay param, TradeType type) {
		// 转换金额格式为元：0.01 ~ 100000000，11位
		param.setTotal_amount(MoneyUtil.toStr(param.getTotal_amount()));
		// 发送请求
		switch (type) {
		case APP:
			return payApp(param);
		case NATIVE:
			return payNative(param);
		case MWEB:
			return payMweb(param);
		default:
			throw ParamRuntimeException.ILLEGAL;
		}
	}
	
	/**
	 * 查询支付
	 * 
	 * 场景：服务、网络异常而未收到支付通知时， 主动查询来确认支付状态
	 * 
	 * @param param
	 * @return 支付状态
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年9月27日
	 */
	public AlipayResultPayQuery queryPay(AlipayParamNo param) {
		// 发起请求
		AlipayTradeQueryResponse response = request(Api.PAY_QUERY, param, new AlipayTradeQueryRequest());
		// 转换结果
		AlipayResultPayQuery result = new AlipayResultPayQuery();
		result.setBuyer_logon_id(response.getBuyerLogonId());
		result.setBuyer_user_id(response.getBuyerUserId());
		result.setTotal_amount(MoneyUtil.toFen(response.getTotalAmount()).toString());
		result.setBuyer_pay_amount(MoneyUtil.toFen(response.getBuyerPayAmount()).toString());
		result.setReceipt_amount(MoneyUtil.toFen(response.getReceiptAmount()).toString());
		result.setTrade_status(TradeStatus.getByStatus(response.getTradeStatus()));
		if (response.getSendPayDate() != null) {
			result.setSend_pay_date(DateUtil.getDateStr(response.getSendPayDate(), DateFormatEnum.DATE_TIME));
		}
		return result;
	}
	
	/**
	 * 退款
	 * 
	 * 场景：交易支付后一定时间内（签约时设置的可退款时间）将支付款按原路退到买家帐号上。单笔交易可以分多次退款，总退款金额不能超过用户实际支付金额
	 * 注意：多次退款需提供原支付订单的商户流水号和不同的退款单号，一笔退款失败后重新提交时要采用原来的退款单号
	 * 
	 * @param param
	 * 
	 * @return 退款订单
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年9月24日
	 */
	public AlipayResultRefund refund(AlipayParamRefund param) {
		// 转换金额格式为元：0.01 ~ 100000000，11位
		param.setRefund_amount(MoneyUtil.toStr(param.getRefund_amount()));		
		// 发起请求
		AlipayTradeRefundResponse response = request(Api.REFUND, param, new AlipayTradeRefundRequest());
		// 转换结果
		AlipayResultRefund result = new AlipayResultRefund();
		result.setBuyer_logon_id(response.getBuyerLogonId());
		result.setBuyer_user_id(response.getBuyerUserId());
		result.setOut_trade_no(response.getOutTradeNo());
		result.setTrade_no(response.getTradeNo());
		result.setRefund_fee(MoneyUtil.toFen(response.getRefundFee()).toString());
		result.setPresent_refund_buyer_amount(MoneyUtil.toFen(response.getPresentRefundBuyerAmount()).toString());
		result.setGmt_refund_pay(DateUtil.getDateStr(response.getGmtRefundPay(), DateFormatEnum.DATE_TIME));
		return result;
	}
	
	/**
	 * 查询退款状态
	 * 
	 * 注意：本接口返回成功仅代表本次查询操作成功，不代表退款成功。如果接口返回了查询数据且refund_status为空或为REFUND_SUCCESS则代表退款成功。
	 * 注意：如果没有查询到则代表未退款成功，可以调用退款接口进行重试，重试时必须保证退款请求号一致。
	 * 
	 * @param param
	 * 
	 * @return 退款状态
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年9月27日
	 */
	public AlipayResultRefundQuery queryRefund(AlipayParamNo param) {
		// 发起请求
		AlipayTradeFastpayRefundQueryResponse response = request(Api.REFUND_QUERY, param, new AlipayTradeFastpayRefundQueryRequest());
		// 转换结果
		AlipayResultRefundQuery result = new AlipayResultRefundQuery();
		result.setOut_request_no(response.getOutRequestNo());
		result.setOut_trade_no(response.getOutTradeNo());
		result.setPresent_refund_buyer_amount(MoneyUtil.toFen(response.getPresentRefundBuyerAmount()).toString());
		result.setRefund_amount(MoneyUtil.toFen(response.getRefundAmount()).toString());
		result.setRefund_reason(response.getRefundReason());
		result.setRefund_status(RefundStatus.getByStatus(response.getRefundStatus()));
		result.setTotal_amount(MoneyUtil.toFen(response.getTotalAmount()).toString());
		result.setTrade_no(response.getTradeNo());
		result.setGmt_refund_pay(DateUtil.getDateStr(response.getGmtRefundPay(), DateFormatEnum.DATE_TIME));
		return result;
	}
	
	/**
	 * 关闭交易
	 * 
	 * 场景：创建交易后，用户长时间未进行支付或取消订单时，可以调用本接口直接关闭未付款的交易，关闭后将不能再支付
	 * 注意：通常在创建支付交易时指定自动关闭时间
	 * 
	 * @param param
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年9月24日
	 */
	public void closeTrade(AlipayParamNo param) {
		// 发起请求
		request(Api.PAY_CLOSE, param, new AlipayTradeCloseRequest());
	}
	
	/**
	 * 下载对账单
	 * 
	 * 说明：获取商户离线账单下载地址，执行下载
	 * 
	 * @param date		账单日期：yyyy-MM-dd，T-1
	 * 
	 * @author 延晓磊
	 * 
	 * @return 账单信息文本，可以存储（.csv）或解析
	 *
	 * @since 2019年9月27日
	 */
	public String bill(String date) {
		return downloadBill(BillMode.TRADE, date);
	}
	
	/**
	 * 下载资金流水账单
	 * 
	 * 说明：获取商户离线账单下载地址，执行下载
	 * 
	 * @param date		账单日期：yyyy-MM-dd，T-1
	 * 
	 * @author 延晓磊
	 * 
	 * @return 账单信息文本，可以存储（.csv）或解析
	 *
	 * @since 2019年9月27日
	 */
	public String flow(String date) {
		return downloadBill(BillMode.SIGNCUSTOMER, date);
	}
	
	/**
	 * 解析业务明细账单（对账单）
	 * 
	 * @param csv	账单信息
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月8日
	 */
	public static List<AlipayBill> parseBill(String csv) {
		// 切割行
		String[] lines = StringUtil.splitLine(csv);
		// 遍历明细信息：去除注释、标题（明细的首字段为数字流水号）
		List<AlipayBill> bills = new ArrayList<>();
		AlipayBill bill;
		String[] rows;
		for (String line : lines) {
			if (StringUtils.isNumeric(line.substring(0, 1))) {
				// 切割列：逗号分隔字段
				rows = StringUtil.split(line);
				// 解析账单
				bill = new AlipayBill();
				bill.setTrade_no(getBillCell(rows, 0));
				bill.setOut_trade_no(getBillCell(rows, 1));
				bill.setType(BillType.getByName(getBillCell(rows, 2)).getCode());
				bill.setSubject(getBillCell(rows, 3));
				bill.setCreateTime(getBillCell(rows, 4));
				bill.setFinishTime(getBillCell(rows, 5));
				bill.setTradeAccount(getBillCell(rows, 9));
				bill.setOrderAmount(MoneyUtil.toFen(getBillCell(rows, 10)).toString());
				bill.setTradeAmount(MoneyUtil.toFen(getBillCell(rows, 11)).toString());
				bill.setOut_request_no(getBillCell(rows, 18));
				bill.setCharge(MoneyUtil.toFen(getBillCell(rows, 19)).toString());
				bill.setRemarks(getBillCell(rows, 21));
				bills.add(bill);
			}
		}
		return bills;
	}
	
	/**
	 * 解析账务账单明细（资金流水账单）
	 * 
	 * @param csv	账单信息
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月8日
	 */
	public static List<AlipayFlow> parseFlow(String csv) {
		// 切割行
		String[] lines = StringUtil.splitLine(csv);
		// 遍历明细信息：去除注释、标题（明细的首字段为数字流水号）
		List<AlipayFlow> bills = new ArrayList<>();
		AlipayFlow bill;
		String[] rows;
		for (String line : lines) {
			if (StringUtils.isNumeric(line.substring(0, 1))) {
				// 切割列：逗号分隔字段
				rows = StringUtil.split(line);
				// 解析账单
				bill = new AlipayFlow();
				bill.setFlowNo(getBillCell(rows, 0));
				bill.setAlipayNo(getBillCell(rows, 1));
				bill.setOrderNo(getBillCell(rows, 2));
				bill.setTradeName(getBillCell(rows, 3));
				bill.setTradeTime(getBillCell(rows, 4));
				bill.setTradeAccount(getBillCell(rows, 5));
				bill.setIncomeAmount(MoneyUtil.toFen(getBillCell(rows, 6)).toString());
				bill.setExpenseAmount(MoneyUtil.toFen(getBillCell(rows, 7)).toString());
				bill.setBalanceAmount(MoneyUtil.toFen(getBillCell(rows, 8)).toString());
				bill.setChannel(getBillCell(rows, 9));
				bill.setBizType(getBillCell(rows, 10));
				bill.setRemarks(getBillCell(rows, 11));
				bill.setFlowType(NumberUtil.isBlank(LongUtil.valueOfZero(bill.getIncomeAmount())) ? FlowType.EXPEND.getCode() : FlowType.INCOME.getCode());
				bills.add(bill);
			}
		}
		return bills;
	}
	
	/**
	 * 支付异步通知（被动）
	 * <pre>
	 * 场景：交易成功后，支付宝以 post 方式请求 notifyUrl，返回异步通知参数
	 * 可靠性：同步通知 < 异步通知 < 支付查询
	 * 
	 * 注意：
	 * 1、须检查通知内容，包括 app_id、out_trade_no、total_amount 是否与请求中的一致，并根据 trade_status 进行后续业务处理。
	 * 2、异步通知必须返回success，否则支付宝服务器会不断重发通知(24小时内)，同一个通知的notify_id相同
	 * </pre>
	 * @param request
	 * @return 通知信息
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月8日
	 */
	public AlipayNotifyPay notifyPay(HttpServletRequest request) {
		// 整理所有参数
		Map<String,String> params = new HashMap<>();
		Map<String,String[]> requestParams = request.getParameterMap();
		for (Entry<String, String[]> entry : requestParams.entrySet()) {
			params.put(entry.getKey(), StringUtil.join(entry.getValue()));
			// 乱码处理
			// params.put(entry.getKey(), StringUtil.iso2Utf(StringUtil.join(entry.getValue())));
		}
		// 验签
		boolean flag;
		try {
			flag = AlipaySignature.rsaCertCheckV1(params, alipayProperties.getAlipayCertPath(), CharsetConstant.UTF8);
		} catch (AlipayApiException e) {
			// 验签出错
			LOG.error(AlipayLogMsg.NOTIFY_ERROR, params, e);
			throw new BizErrorRuntimeException(AlipayMsg.NOTIFY_CHECK_SIGN_ERROR, e);
		}
		if (!flag) {
			// 验签出错
			LOG.error(AlipayLogMsg.NOTIFY_ERROR, params);
			throw new BizRuntimeException(AlipayMsg.NOTIFY_CHECK_SIGN_ERROR);
		}
		// 返回通知
		return ReflectUtil.toObj(params, AlipayNotifyPay.class);
	}
 
	
	
	
	
	
	/**
	 * 发起请求
	 * 
	 * @param api		接口
	 * @param param		参数
	 * @param request	请求对象
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月17日
	 */
	private <T extends AlipayResponse>T request(Api api, AlipayParam param, AlipayRequest<T> request){
		// 请求参数
		request.setBizModel(param);
		// 回调地址
		setNotifyUrls(api, request);
		// 发起请求
		T response;
		try {
			switch (api) {
			case PAY_NATIVE:
			case PAY_MWEB:
				response = alipayClient.pageExecute(request);
				break;
			case PAY_APP:
				response = alipayClient.sdkExecute(request);
				break;
			default:
				response = alipayClient.execute(request);
				break;
			}
		} catch (Exception e) {
			// 请求出错
			throw new BizErrorRuntimeException(AlipayMsg.REQUEST_ERROR, e);
		}
		// 验证结果
		if (response.isSuccess()) {
			// 成功
			return response;
		}else {
			// 失败
			String reqMsg = JsonUtil.toStr(request);
			String repMsg = JsonUtil.toStr(response);
			LOG.error(AlipayLogMsg.REQUEST_ERROR, api.getName(), reqMsg, repMsg);
			throw new BizRuntimeException(response.getSubMsg());
		}
	}
	
	/**
	 * 设置请求的通知回调URL、H5跳转URL
	 * 
	 * @param api		接口
	 * @param request	请求
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年9月24日
	 */
	private void setNotifyUrls(Api api, AlipayRequest<?> request) {
		switch (api) {
		case PAY_MWEB:
			/* 同步回调地址
			 * 
			 * ----- PC网站/APP -----
			 * 同步结果通知，忽略。
			 * 用户在支付宝支付成功后，支付宝会以GET方式请求该地址，附带支付结果数据，但准确性不可靠，必须异步通知或查询接口返回为准
			 * 
			 * ----- H5网站 -----
			 * 前端回调跳转地址。
			 * 1、支付完成后，支付宝请求return_url跳转到前端页面，附带支付订单参数（out_trade_no商户订单号，trade_no支付宝订单号）
			 * 2、前端通过out_trade_no请求服务端，获取支付订单信息或状态，完成后续操作
			 */
			request.setReturnUrl(alipayProperties.getReturnUrl());
		case PAY_APP:
		case PAY_NATIVE:
			/*
			 * 异步回调地址
			 * 
			 * 用户在支付宝支付成功后，支付宝会在24小时内以POST方式间隔该地址，直至收到success
			 * 异步回调是处理支付状态的主要策略，如果24小时内服务未能处理，则主动使用支付查询接口处理
			 */
			request.setNotifyUrl(alipayProperties.getNotifyUrl());
			break;
		default:
			break;
		}
	}
	
	/**
	 * 申请支付 - 扫码支付（PC网页）
	 * 
	 * 流程：申请支付(S) > 展示支付页面(C) > 扫码支付(U) > 间隔查询服务端支付状态来刷新界面(C)/取消支付时返回到request_from_url(W)
	 * 
	 * @param param
	 * 
	 * @return 支付订单
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年9月24日
	 */
	private AlipayResultPay payNative(AlipayParamPay param) {
		// 发起请求
		AlipayTradePagePayResponse response = request(Api.PAY_NATIVE, param, new AlipayTradePagePayRequest());
		// 转换结果
		AlipayResultPay result = new AlipayResultPay();
		result.setMerchant_order_no(response.getMerchantOrderNo());
		result.setOut_trade_no(response.getOutTradeNo());
		result.setSeller_id(response.getSellerId());
		result.setTotal_amount(MoneyUtil.toFen(response.getTotalAmount()).toString());
		result.setTrade_no(response.getTradeNo());
		// 调起支付宝的HTML页面内容
		result.setBody(response.getBody());
		return result;
	}
	
	/**
	 * 申请支付 - APP支付
	 * 
	 * 流程：申请支付(S) > 调起微信(C) > 扫码支付(U) > 间隔查询服务端支付状态来刷新界面(C)/取消支付时返回(C)
	 * 
	 * @param param
	 * 
	 * @return 支付订单
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年9月24日
	 */
	private AlipayResultPay payApp(AlipayParamPay param) {
		// 发起请求
		AlipayTradeAppPayResponse response = request(Api.PAY_APP, param, new AlipayTradeAppPayRequest());
		// 转换结果
		AlipayResultPay result = new AlipayResultPay();
		result.setMerchant_order_no(response.getMerchantOrderNo());
		result.setOut_trade_no(response.getOutTradeNo());
		result.setSeller_id(response.getSellerId());
		result.setTotal_amount(MoneyUtil.toFen(response.getTotalAmount()).toString());
		result.setTrade_no(response.getTradeNo());
		return result;
	}
	
	/**
	 * 申请支付 - H5支付
	 * 
	 * 流程：申请支付(S) > 展示支付页面(C) > 调起支付(U) > 间隔查询服务端支付状态来刷新界面(C)/取消支付时返回到quit_url(W)
	 * 
	 * @param param	
	 * 
	 * @return 支付订单
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年9月24日
	 */
	private AlipayResultPay payMweb(AlipayParamPay param) {
		// 发起请求
		AlipayTradeWapPayResponse response = request(Api.PAY_MWEB, param, new AlipayTradeWapPayRequest());
		// 转换结果
		AlipayResultPay result = new AlipayResultPay();
		result.setMerchant_order_no(response.getMerchantOrderNo());
		result.setOut_trade_no(response.getOutTradeNo());
		result.setSeller_id(response.getSellerId());
		result.setTotal_amount(MoneyUtil.toFen(response.getTotalAmount()).toString());
		result.setTrade_no(response.getTradeNo());
		// 调起支付宝的HTML页面内容
		result.setBody(response.getBody());
		return result;
	}
	
	/**
	 * 对账日期校验：T-1
	 * 
	 * @param date
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月8日
	 */
	private static void checkBillDate(String date) {
		// 日账单：必须是昨天之前
		if (!LocalDateUtil.toLocalDate(date).isBefore(LocalDate.now())) {
			throw new ParamRuntimeException(AlipayMsg.TRADE_BILL_DATA_ERROR);
		}
	}
	
	/**
	 * 下载对账单/资金流水账单
	 * 
	 * 说明：获取商户离线账单下载地址，执行下载
	 * 
	 * @param mode		账单类型
	 * @param date		账单日期：yyyy-MM-dd，T-1
	 * 
	 * @author 延晓磊
	 * 
	 * @return 账单信息文本，可以存储（.csv）或解析
	 *
	 * @since 2019年9月27日
	 */
	private String downloadBill(String mode, String date) {
		// 校验日期
		checkBillDate(date);
		// 发起请求
		AlipayParamBill param = new AlipayParamBill();
		param.setBill_date(date);
		param.setBill_type(mode);
		AlipayDataDataserviceBillDownloadurlQueryResponse response = request(Api.DOWNLOAD_BILL, param, new AlipayDataDataserviceBillDownloadurlQueryRequest());
		// 下载账单，30秒内有效
		return StringUtils.toEncodedString(HttpClientUtil.doGetBytes(response.getBillDownloadUrl()), CharsetConstant.GBK_CHARSET);
	}
	
	/**
	 * 获取对账单字段值
	 * 
	 * @param rows
	 * @param index
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月16日
	 */
	private static String getBillCell(String[] rows, int index) {
		return StringUtils.trimToEmpty(ArrayUtil.get(rows, index));
	}
	
}
